Working with
Factors

Day 12

Prof Emily Kurtz

Carleton College
Stat 220 - Spring 2026

Survivor castaways data

# A tibble: 1,417 × 28
   version version_season season full_name      castaway_id castaway   age city 
   <fct>   <chr>           <dbl> <chr>          <chr>       <chr>    <dbl> <chr>
 1 US      US50               50 Angelina Keel… US0554      Angelina    NA <NA> 
 2 US      US50               50 Aubry Bracco   US0477      Aubry       NA <NA> 
 3 US      US50               50 Charlie Davis  US0682      Charlie     NA <NA> 
 4 US      US50               50 Chrissy Hofbe… US0515      Chrissy     NA <NA> 
 5 US      US50               50 Christian Hub… US0550      Christi…    NA <NA> 
 6 US      US50               50 Cirie Fields   US0179      Cirie       NA <NA> 
 7 US      US50               50 Benjamin Wade  US0277      Coach       NA <NA> 
 8 US      US50               50 Colby Donalds… US0031      Colby       NA <NA> 
 9 US      US50               50 Dee Valladares US0666      Dee         NA <NA> 
10 US      US50               50 Emily Flippen  US0668      Emily       NA <NA> 
# ℹ 1,407 more rows
# ℹ 20 more variables: state <chr>, episode <dbl>, day <dbl>, order <dbl>,
#   result <chr>, jury_status <chr>, place <int>, original_tribe <chr>,
#   jury <lgl>, finalist <lgl>, winner <lgl>, acknowledge <lgl>,
#   ack_look <lgl>, ack_speak <lgl>, ack_gesture <lgl>, ack_smile <lgl>,
#   ack_quote <chr>, ack_score <int>, jury1 <dbl>, jury2 <fct>

Both of these are categorical variables

unique(castaways$version)
[1] US AU SA UK NZ
Levels: AU NZ SA UK US
unique(castaways$full_name) %>% head()
[1] "Angelina Keeley"   "Aubry Bracco"      "Charlie Davis"    
[4] "Chrissy Hofbeck"   "Christian Hubicki" "Cirie Fields"     

But are stored as different types

class(castaways$version)
[1] "factor"
class(castaways$full_name) 
[1] "character"

Same graph, colored by the same categorical variable

Factors

R’s representation of categorical data. Consists of:

  1. A set of values

  2. An ordered set of valid levels

eyes <- factor(x = c("blue", "green", "green"), 
               levels = c("blue", "brown", "green"))
eyes
[1] blue  green green
Levels: blue brown green

Factors

Stored as an integer vector with a levels attribute

unclass(eyes)
[1] 1 3 3
attr(,"levels")
[1] "blue"  "brown" "green"

  • Simple functions for working with factors.

  • Part of the tidyverse

# loaded with tidyverse
library(forcats)

gss_cat

A sample of data from the General Social Survey, a long-running US survey conducted by NORC at the University of Chicago.

# A tibble: 21,483 × 9
    year marital         age race  rincome        partyid    relig denom tvhours
   <int> <fct>         <int> <fct> <fct>          <fct>      <fct> <fct>   <int>
 1  2000 Never married    26 White $8000 to 9999  Ind,near … Prot… Sout…      12
 2  2000 Divorced         48 White $8000 to 9999  Not str r… Prot… Bapt…      NA
 3  2000 Widowed          67 White Not applicable Independe… Prot… No d…       2
 4  2000 Never married    39 White Not applicable Ind,near … Orth… Not …       4
 5  2000 Divorced         25 White Not applicable Not str d… None  Not …       1
 6  2000 Married          25 White $20000 - 24999 Strong de… Prot… Sout…      NA
 7  2000 Never married    36 White $25000 or more Not str r… Chri… Not …       3
 8  2000 Divorced         44 White $7000 to 7999  Ind,near … Prot… Luth…      NA
 9  2000 Married          44 White $25000 or more Not str d… Prot… Other       0
10  2000 Married          47 White $25000 or more Strong re… Prot… Sout…       3
# ℹ 21,473 more rows

Warm up

Use gss_cat to answer the following questions.

  1. Which religions watch the least TV?

  2. Do married people watch more or less TV than single people?

TV Viewership by Religion

gss_cat %>%
  drop_na(tvhours) %>%
  group_by(relig) %>%
  summarize(tvhours = mean(tvhours)) %>%
  ggplot(aes(tvhours, relig)) +
    geom_point()

Which do you prefer?

Why is the y-axis in this order?

levels()

Use levels() to access a factor’s levels

gss_cat %>% pull(relig) %>% levels()
 [1] "No answer"               "Don't know"             
 [3] "Inter-nondenominational" "Native american"        
 [5] "Christian"               "Orthodox-christian"     
 [7] "Moslem/islam"            "Other eastern"          
 [9] "Hinduism"                "Buddhism"               
[11] "Other"                   "None"                   
[13] "Jewish"                  "Catholic"               
[15] "Protestant"              "Not applicable"         

Most useful factor skills:

1. Reorder the levels

2. Recode the levels

3. Collapse levels

fct_reorder

  • .f factor vector
  • .x variable to reorder by (in conjunction with .fun)
  • .fun function to reorder by
  • .desc put levels in descending order?
fct_reorder(
  .f, 
  .x, 
  .fun = median, 
  ...,
  .desc = FALSE 
  )

Reorder relig by tvhours

gss_cat %>%
  drop_na(tvhours) %>%
  group_by(relig) %>%
  summarize(tvhours = mean(tvhours)) %>%
  ggplot(aes(x = tvhours)) +
  aes(y = relig) +                      
  geom_point()

Reorder relig by tvhours

gss_cat %>%
  drop_na(tvhours) %>%
  group_by(relig) %>%
  summarize(tvhours = mean(tvhours)) %>%
  ggplot(aes(x = tvhours)) +
  aes(y = fct_reorder(relig, tvhours)) +
  geom_point()

Try it

Use rincome_summary to construct a dotplot of rincome against age.

Reorder rincome by age

rincome_summary <- gss_cat %>%
  group_by(rincome) %>%
  summarize(
    age = mean(age, na.rm = TRUE),
    tvhours = mean(tvhours, na.rm = TRUE),
    n = n()
  )

Which do you prefer?

fct_reorder2

Reorders the levels of a factor by the Y values associated with the largest X values.

  • .f factor vector
  • .x X variable
  • .y Y variable
  • .fun function to reorder by
  • .desc put levels in descending order?
fct_reorder2(
  .f, 
  .x, 
  .y, 
  .fun = median, 
  ...,
  .desc = FALSE
  )

Reorder marital

gss_cat %>%
  drop_na(age) %>%
  count(age, marital) %>%
  group_by(age) %>%
  mutate(prop = n / sum(n)) %>%
  ggplot(aes(x = age, y = prop)) +
  aes(color = marital) +                                     
  geom_line() +
  scale_color_colorblind("")

Reorder marital

gss_cat %>%
  drop_na(age) %>%
  count(age, marital) %>%
  group_by(age) %>%
  mutate(prop = n / sum(n)) %>%
  ggplot(aes(x = age, y = prop)) +
  aes(color = fct_reorder2(marital, .x = age, .y = prop)) +
  geom_line() +
  scale_color_colorblind("")

Other reordering functions

gss_cat %>%
  ggplot() + 
  geom_bar(aes(x = marital))

Other reordering functions

gss_cat %>%
  ggplot() + 
  geom_bar(aes(x = fct_infreq(marital)))

Other reordering functions

gss_cat %>%
  ggplot() + 
  geom_bar(aes(x = fct_rev(fct_infreq(marital))))

Which political leaning watches more TV?

How could we improve the partyid labels?

fct_recode

Changes values of levels

  • .f factor vector
  • ... new level = old level pairs (as a named character vector)
fct_recode(.f, ...)

Recoding partyid

gss_cat %>%
  drop_na(tvhours) %>%
  select(partyid, tvhours) %>%
    mutate(partyid = fct_recode(partyid,
    "Republican, strong"    = "Strong republican",
    "Republican, weak"      = "Not str republican",
    "Independent, near rep" = "Ind,near rep",
    "Independent, near dem" = "Ind,near dem",
    "Democrat, weak"        = "Not str democrat",
    "Democrat, strong"      = "Strong democrat")) %>% 
  group_by(partyid) %>%
  summarize(tvhours = mean(tvhours)) %>%
  ggplot(aes(tvhours, fct_reorder(partyid, tvhours))) +
  geom_point() + 
  labs(y = "partyid")

How can we combine these factor levels?

fct_collapse()

Changes multiple levels into single levels

  • .f factor vector
  • ... named arguments set to a character vector (levels in the vector will be collapsed to the name of the argument)
fct_collapse(.f, ...)

Collapsing partyid

gss_cat %>%
  drop_na(tvhours) %>%
  select(partyid, tvhours) %>%
  mutate(
    partyid = 
      fct_collapse(
        partyid,
        conservative = c("Strong republican", 
                         "Not str republican", 
                         "Ind,near rep"),
        liberal = c("Strong democrat", 
                    "Not str democrat", 
                    "Ind,near dem"))
  ) %>% 
  group_by(partyid) %>%
  summarize(tvhours = mean(tvhours)) %>%
  ggplot(aes(tvhours, fct_reorder(partyid, tvhours))) +
  geom_point() + 
  labs(y = "partyid")

Your turn

Collapse the marital variable to have levels Married, Not married, and No answer

Include "Never married", "Divorced", and “Widowed" in Not married

There are relatively few points in each of these groups

There are relatively few points in each of these groups

fct_lump()

Collapses levels with fewest values into a single level.

By default collapses as many levels as possible such that the new level is still the smallest.

  • f factor vector
  • n preserve the most common n levels, lump together the rest for n+1 total levels
  • other_level = "Other"
fct_lump(
  f, 
  n, 
  other_level = "Other", 
  ...
)

Lumping partyid

gss_cat %>%
  mutate(partyid = partyid) %>%                    
  ggplot(aes(x = partyid)) + 
  geom_bar() +
  labs(x = "partyid")

Lumping partyid

gss_cat %>%
  mutate(partyid = fct_lump(partyid, n = 2)) %>% 
  ggplot(aes(x = partyid)) + 
  geom_bar() +
  labs(x = "partyid")

Lumping partyid

gss_cat %>%
  mutate(partyid = fct_lump(partyid, n = 3)) %>%
  ggplot(aes(x = partyid)) + 
  geom_bar() +
  labs(x = "partyid")

Your turn: hotel bookings

The remainder of the activity file has you fixing up some plots using hotel bookings data from Tidy Tuesday